﻿using System;
using System.Activities;
using System.Activities.Hosting;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.DurableInstancing;
using System.Threading;
using System.Threading.Tasks;
using MvcFlow.Diagnostics;
using MvcFlow.Extensions;

namespace MvcFlow.Execution
{
    public class AsyncWorkflowApplication : IDisposable
    {
        private static readonly ILog                                log = LogManager.GetLog("Execution");

        private readonly string                                     inputValueName;
        private readonly string                                     outputValueName;
        private readonly InstanceStore                              instanceStore;
        private readonly Action<WorkflowInstanceExtensionManager>   extensionRegistrationCallback;
        private readonly Func<SynchronizationContext>               synchronizationContextAccessor;

        public async Task<WorkflowStepResult> ExecuteStepAsync(Activity activity, object inputValue, Guid? instanceId = null, string bookmarkName = null)
        {
            var completionSource    = new TaskCompletionSource<WorkflowStepResult>();
            var isContinuation      = instanceId != null && bookmarkName != null;
            var application         = BuildApplication(activity, inputValue,isContinuation,completionSource);

            if (isContinuation)
            {
                log.Debug("Loading workflow state for bookmark resume");

                try
                {
                    await application.LoadAsync(instanceId.Value);

                    if (application.GetBookmarks().FirstOrDefault(b => b.BookmarkName == bookmarkName) != null)
                    {
                        log.Debug("Resuming workflow from bookmark {0}",bookmarkName);
                        await application.ResumeBookmarkAsync(new Bookmark(bookmarkName), inputValue);
                    }
                    else
                    {
                        log.Error("Bookmark {0} not found", bookmarkName);
                    }
                }
                catch (AggregateException ex)
                {
                    if ( ex.InnerExceptions.Any(e => e is InstanceCompleteException))
                    {
                        log.Warning("An old instance id was passed to a workflow execution step {0}", instanceId);
                    }

                    throw;
                }
            }
            else
            {
                log.Debug("Starting workflow execution");
                await application.RunAsync();
            }

            var result = await completionSource.Task;
            log.Debug("Workflow execution step complete");

            if (result.BookmarkName != null)
            {
                await application.UnloadAndWaitAsync();
            }

            return result;
        }

        public void Dispose()
        {}

        private void HookApplicationEvents(WorkflowApplication application,IBookmarkOutputExtension bookmarkOutput,TaskCompletionSource<WorkflowStepResult> completionSource)
        {
            application.OnUnhandledException += args =>
            {
                SendOrPostCallback callback = state => UnhandledException(args, completionSource);
                application.SynchronizationContext.Post(callback, null);

                return UnhandledExceptionAction.Terminate;
            };

            application.Completed += args =>
            {
                SendOrPostCallback callback = state => Completed(args, completionSource);
                application.SynchronizationContext.Post(callback, null);
            };

            application.Idle += args =>
            {
                SendOrPostCallback callback = state => Idle(args, completionSource);
                application.SynchronizationContext.Post(callback, null);
            };

            application.PersistableIdle += args =>
            {
                SendOrPostCallback callback = state => PersistableIdle(args, bookmarkOutput, completionSource);
                application.SynchronizationContext.Post(callback, null);

                return PersistableIdleAction.Persist;
            };

            application.Aborted += args =>
            {
                SendOrPostCallback callback = state => Aborted(args, completionSource);
                application.SynchronizationContext.Post(callback, null);
            };
        }

        private WorkflowApplication BuildApplication(Activity activity,object inputValue,bool isContinuation,TaskCompletionSource<WorkflowStepResult> completionSource )
        {
            var application = isContinuation ?  new WorkflowApplication(activity) : 
                                                new WorkflowApplication(activity, new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase) { { inputValueName, inputValue } });

            application.InstanceStore           = instanceStore;
            application.SynchronizationContext  = synchronizationContextAccessor();

            var bookmarkOutput = new ConcurrentBookmarkOutputExtension();
            application.Extensions.Add(bookmarkOutput);

            if (extensionRegistrationCallback != null)
            {
                extensionRegistrationCallback(application.Extensions);
            }

            HookApplicationEvents(application,bookmarkOutput,completionSource);

            return application;
        }

        private void Aborted(WorkflowApplicationAbortedEventArgs args, TaskCompletionSource<WorkflowStepResult> completionSource)
        {
            log.ErrorException("Workflow aborted", args.Reason);

            completionSource.TrySetException(args.Reason);
        }

        private void PersistableIdle(WorkflowApplicationIdleEventArgs args, IBookmarkOutputExtension bookmarkOutput, TaskCompletionSource<WorkflowStepResult> completionSource)
        {
            log.Debug("Workflow persistable idle");

            foreach (var bookmark in args.Bookmarks)
            {
                log.Debug("Workflow bookmark encountered: {0}", bookmark.BookmarkName);
            }

            var bookmarkToUse = args.Bookmarks.FirstOrDefault();

            if (bookmarkToUse != null)
            {
                object outputValue = null;

                if (bookmarkOutput!= null)
                {
                    outputValue = bookmarkOutput.GetBookmarkValue(bookmarkToUse.BookmarkName);
                }

                log.Debug("Bookmark output value is {0}", outputValue);

                completionSource.TrySetResult(new WorkflowStepResult(outputValue, bookmarkToUse.BookmarkName, args.InstanceId));
            }
            else
            {
                log.Error("No bookmark encountered, cannot interrupt");
            }
        }

        private void Idle(WorkflowApplicationIdleEventArgs args, TaskCompletionSource<WorkflowStepResult> completionSource)
        {
            log.Debug("Workflow idle");
        }

        private void Completed(WorkflowApplicationCompletedEventArgs args, TaskCompletionSource<WorkflowStepResult> completionSource)
        {
            log.Debug("Workflow completed");

            object outputValue = null;
            args.Outputs.TryGetValue(outputValueName, out outputValue);
            completionSource.TrySetResult(new WorkflowStepResult(outputValue, null, args.InstanceId));
        }

        private void UnhandledException(WorkflowApplicationUnhandledExceptionEventArgs args, TaskCompletionSource<WorkflowStepResult> completionSource)
        {
            log.ErrorException("Workflow unhandled exception", args.UnhandledException);
            completionSource.SetException(args.UnhandledException);
        }

        internal AsyncWorkflowApplication(InstanceStore instanceStore,Action<WorkflowInstanceExtensionManager> extensionRegistrationCallback,Func<SynchronizationContext> synchronizationContextAccessor = null,string inputValueName = null, string outputValueName = null)
        {
            this.instanceStore                  = instanceStore;
            this.extensionRegistrationCallback  = extensionRegistrationCallback;
            this.synchronizationContextAccessor = synchronizationContextAccessor ?? (() => SynchronizationContext.Current);
            this.inputValueName                 = inputValueName ?? "InputValue";
            this.outputValueName                = outputValueName ?? "OutputValue";
        }
    }
}